package com.xgzit.xgzmall.seckill.service.impl;

import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.sun.deploy.security.BlockedException;
import com.xgzit.common.dto.mq.SeckillOrderDto;
import com.xgzit.common.utils.R;
import com.xgzit.common.vo.MemberEntityVo;
import com.xgzit.xgzmall.seckill.dto.SeckillSkuRedisDto;
import com.xgzit.xgzmall.seckill.feign.CouponFeignService;
import com.xgzit.xgzmall.seckill.feign.ProductFeignService;
import com.xgzit.xgzmall.seckill.interceptor.LoginUserInterceptor;
import com.xgzit.xgzmall.seckill.service.SeckillService;
import com.xgzit.xgzmall.seckill.vo.SeckillSessionWithSkusVo;
import com.xgzit.xgzmall.seckill.vo.SkuInfoVo;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import javax.swing.text.html.parser.Entity;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * @Author xgz
 * @Date 2021-07-11 21:39
 */
@Slf4j
@Service
public class SeckillServiceImpl implements SeckillService {

    /**
     * 活动缓存前缀
     **/
    private final String SESSIONS_CACHE_PREFIX = "seckill:sessions:";

    /**
     * hash 的缓存
     **/
    private final String SKUKILL_CACHE_PREFIX = "seckill:skus";

    /**
     * 信号量前缀  后面带商品随机码
     **/
    private final String SKU_STOCK_SEMAPHORE = "seckill:stock:";

    @Autowired
    CouponFeignService couponFeignService;

    @Autowired
    ProductFeignService productFeignService;

    @Autowired
    StringRedisTemplate redisTemplate;

    @Autowired
    RedissonClient redissonClient;

    @Autowired
    RabbitTemplate rabbitTemplate;

    @Override
    public void uploadSecKillSkuLatestThreeDays() {
        // 1、扫描需要参与秒杀的活动 【需要调用优惠系统】
        R r = couponFeignService.getLatestThreeDaySession();
        if (r.getCode() == 0) {
            List<SeckillSessionWithSkusVo> data = r.getData(new TypeReference<List<SeckillSessionWithSkusVo>>() {
            });
            // 上架商品  缓存活动信息、活动商品信息到redis
            if (Objects.nonNull(data) && !CollectionUtils.isEmpty(data)) {
                // 保存活动信息
                saveSessionInfos(data);
                // 保存活动商品信息
                saveSessionSkuInfos(data);
            }
        }
    }
    // SentinelResource 指定资源名称，blockHandler：被限流、降级、系统保护时被调用 的方法，必须是同类下同返回值的函数
    // fallback 针对所有类型异常都会调用。即这个方法有异常就调用。 如果不是同类下的函数，必须是静态方法。
    @SentinelResource(value = "getCurrentSeckillSkusResource", blockHandler = "getCurrentSeckillSkusBlockHandler")
    @Override
    public List<SeckillSkuRedisDto> getCurrentSeckillSkus() {
        // 确定当前时间
        long time = System.currentTimeMillis();
        // 获取所有的指定key
        // todo 自定义受保护的资源  Entry entry =  SphU.entry("SeckillSkus") 资源取个名字
        try(Entry entry =  SphU.entry("SeckillSkus")){
            Set<String> keys = redisTemplate.keys(SESSIONS_CACHE_PREFIX + "*");
            if (!CollectionUtils.isEmpty(keys)) {
                for (String key : keys) {
//                seckill:sessions:1626307200000_1626393599000
                    String replace = key.replace(SESSIONS_CACHE_PREFIX, "");
                    String[] s = replace.split("_");
                    long start = Long.parseLong(s[0]);
                    long end = Long.parseLong(s[1]);
                    log.info("{}===>{}===>{}", start, time, end);
                    // 如果当前时间在这个 秒杀区间内
                    if (time >= start && time <= end) {
                        // 获取当前场次所有信息  range 是取一个区间 对应 lrange
                        List<String> range = redisTemplate.opsForList().range(key, -1000, 1000);
                        // 绑定 hash 键
                        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
                        // 获取多个 key 的值
                        List<String> list = hashOps.multiGet(range);
                        if (Objects.nonNull(list) && !CollectionUtils.isEmpty(list)) {
                            return list.stream().map(item -> {
                                //                            dto.setRandomCode(null); // 当前秒杀开始了，需要随机码
                                return JSON.parseObject(item, SeckillSkuRedisDto.class);
                            }).collect(Collectors.toList());
                        }
                        break;
                    }
                }
            }
        }catch (BlockException e){
            log.error("资源被限流，exception={}",e.getMessage());
        }
        return new ArrayList<>();
    }

    public List<SeckillSkuRedisDto> getCurrentSeckillSkusBlockHandler(BlockException e){
        log.error("getCurrentSeckillSkus 被限流了", e);
        return new ArrayList<>();
    }

    @Override
    public SeckillSkuRedisDto getSkuSeckillInfo(Long skuId) {
        // 找到所有需要参与秒杀的key
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
        Set<String> keys = hashOps.keys();
        if (!CollectionUtils.isEmpty(keys)) {
            String regx = "\\d_" + skuId;
            for (String key : keys) {
                // 6_4 使用正则匹配
                if (Pattern.matches(regx, key)) {
                    String json = hashOps.get(key);
                    SeckillSkuRedisDto skuRedisDto = JSON.parseObject(json, SeckillSkuRedisDto.class);
                    // 处理随机码
                    Long startTime = skuRedisDto.getStartTime();
                    Long endTime = skuRedisDto.getEndTime();
                    long time = System.currentTimeMillis();
                    if (time >= startTime && time <= endTime) {
                        return skuRedisDto;
                    } else {
                        skuRedisDto.setRandomCode(null);
                        return skuRedisDto;
                    }
                }

            }
        }
        return null;
    }

    @Override
    public String kill(String killId, Integer num, String key) {
        MemberEntityVo memberEntityVo = LoginUserInterceptor.loginUser.get();
        Long userId = memberEntityVo.getId();
        // 秒杀服务 队列削峰
        BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
        String json = hashOps.get(killId);
        if (StringUtils.isEmpty(killId)) {
            return null;
        } else {
            SeckillSkuRedisDto redisDto = JSON.parseObject(json, SeckillSkuRedisDto.class);
            // 1、合法性校验 秒杀时间、随机码、商品id
            Long startTime = redisDto.getStartTime();
            Long endTime = redisDto.getEndTime();
            long current = System.currentTimeMillis();
            long ttl = endTime - current;
            if (current >= startTime && current <= endTime) {
                // 校验随机码
                String randomCode = redisDto.getRandomCode();
                String sessionSkuId = redisDto.getPromotionSessionId() + "_" + redisDto.getSkuId();
                if (randomCode.equals(key) && killId.equals(sessionSkuId)) {
                    // 校验数量
                    if (num <= redisDto.getSeckillLimit()) {
                        // 校验是否已经购买过。 幂等性 只要秒杀成功，就去占位， userId_SessionId_skuId;
                        // SETNX 不存在才新增。 占位成功表示没有买过, 该占位要自动过期，在秒杀之后
                        String redisKey = userId + "_" + sessionSkuId;
                        Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(redisKey, String.valueOf(num), ttl, TimeUnit.MILLISECONDS);
                        if (aBoolean) {
                            // 占位成功，才可以继续。否则说明已经买过了
                            RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
                            // 信号量扣减，必须使用非阻塞的方法，扣减成功表示购买成功
//                                boolean b = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
                            boolean b = semaphore.tryAcquire(num);
                            if (b) {
                                // 秒杀成功  快速下单 使用 IdWorker 创建订单号
                                String orderSn = IdWorker.getTimeId();
                                SeckillOrderDto orderDto = new SeckillOrderDto();
                                orderDto.setOrderSn(orderSn);
                                orderDto.setMemberId(userId);
                                orderDto.setSeckillPrice(redisDto.getSeckillPrice());
                                orderDto.setNum(num);
                                orderDto.setSkuId(redisDto.getSkuId());
                                orderDto.setPromotionSessionId(redisDto.getPromotionSessionId());
                                rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", orderDto);
                                return orderSn;
                            }
                        }
                    }
                }
            } else {
                return null;
            }
        }

        return null;
    }

    private void saveSessionInfos(List<SeckillSessionWithSkusVo> sessions) {
        if (Objects.nonNull(sessions)&& !CollectionUtils.isEmpty(sessions)){
            sessions.forEach(session -> {
                long startTime = session.getStartTime().getTime();
                long endTime = session.getEndTime().getTime();
                String key = SESSIONS_CACHE_PREFIX + startTime + "_" + endTime;
                // 缓存活动信息， 幂等性处理, 没有key才进行保存
                Boolean hasKey = redisTemplate.hasKey(key);
                if (!hasKey) {
                    //  商品信息使用 场次id下划线商品id
                    List<String> collect = session.getRelationSkus().stream().map(item -> item.getPromotionSessionId() + "_" + item.getSkuId().toString()).collect(Collectors.toList());
                    redisTemplate.opsForList().leftPushAll(key, collect);
                }

            });
        }

    }

    private void saveSessionSkuInfos(List<SeckillSessionWithSkusVo> sessions) {
        sessions.forEach(session -> {
            // 准备hash 操作
            BoundHashOperations<String, Object, Object> hashOps = redisTemplate.boundHashOps(SKUKILL_CACHE_PREFIX);
            session.getRelationSkus().forEach(seckillSkuVo -> {
                // 缓存商品幂等性处理
                if (!hashOps.hasKey(seckillSkuVo.getPromotionSessionId().toString() + "_" + seckillSkuVo.getSkuId().toString())) {
                    // 缓存商品
                    SeckillSkuRedisDto skuRedisDto = new SeckillSkuRedisDto();
                    // 1、 sku 基本数据 远程查询
                    R r = productFeignService.getSkuInfo(seckillSkuVo.getSkuId());
                    if (r.getCode() == 0) {
                        SkuInfoVo info = r.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                        });
                        skuRedisDto.setSkuInfo(info);
                    }
                    // 2、 sku 秒杀信息
                    BeanUtils.copyProperties(seckillSkuVo, skuRedisDto);
                    // 3、秒杀时间信息
                    skuRedisDto.setStartTime(session.getStartTime().getTime());
                    skuRedisDto.setEndTime(session.getEndTime().getTime());
                    // 4、 随机码 【引入随机码，只有秒杀开始时才暴露，只有带随机码才能秒杀】
                    String randomCode = UUID.randomUUID().toString().replace("-", "");
                    skuRedisDto.setRandomCode(randomCode);
                    String jsonString = JSON.toJSONString(skuRedisDto);
                    // key 也要是 场次id下划线商品id
                    hashOps.put(seckillSkuVo.getPromotionSessionId().toString() + "_" + seckillSkuVo.getSkuId().toString(), jsonString);
                    // 库存的 幂等性 如果当前商品的库存信息已经上架就不需要再上架
                    // 5、商品的 分布式信号量  就是当前商品的库存信息，主要是库存不能去数据库实时扣减
                    // 信号量的扣减也必须带上 随机码，不然没开始你就可以去扣减库存了
                    RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
                    // 商品可以秒杀的数量 作为信号量 限流
                    semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
                }
            });
        });
    }
}
